#!/usr/bin/python3
"""
Configure chrony to set system time from the network.

Configures `chrony` by modifying `/etc/chrony.conf`.

Before new values are added to the chrony configuration, all lines starting with
"server", "pool" or "peer" are removed.

The 'timeservers' option provides a very high-level way of configuring chronyd
with specific timeservers. Its value is a list of strings representing the
hostname or IP address of the timeserver. For each list item, the following
line will be added to the configuration:
`server <HOSTNAME/IP> iburst`

The 'servers' option provides a direct mapping to the `server` directive from
chrony configuration. Its value is a list of dictionaries representing each
timeserver which should be added to the configuration. For each list item,
a `server` directive will be added the configuration. Currently supported
subset of options which can be specified for each timeserver item:
  - 'hostname' (REQUIRED)
  - 'minpoll'
  - 'maxpoll'
  - 'iburst' (defaults to true)
  - 'prefer' (defaults to false)

The 'leapsectz' option configures chrony behavior related to automatic checking
of the next occurrence of the leap second, using the provided timezone. Its
value is a string representing a timezone from the system tz database (e.g.
'right/UTC'). If an empty string is provided, then all occurrences of
'leapsectz' directive are removed from the configuration.

Constraints:
  - Exactly one of 'timeservers' or 'servers' options must be provided.
"""


import sys
import re

import osbuild.api


SCHEMA = """
"additionalProperties": false,
"oneOf": [
  {"required": ["timeservers"]},
  {"required": ["servers"]}
],
"properties": {
  "timeservers": {
    "type": "array",
    "items": { "type": "string" },
    "description": "Array of NTP server addresses."
  },
  "servers": {
    "type": "array",
    "items": {
      "additionalProperties": false,
      "type": "object",
      "required": ["hostname"],
      "properties": {
        "hostname": {
          "type": "string",
          "description": "Hostname or IP address of a NTP server."
        },
        "minpoll": {
          "type": "integer",
          "description": "Specifies the minimum interval between requests sent to the server as a power of 2 in seconds.",
          "minimum": -6,
          "maximum": 24
        },
        "maxpoll": {
          "type": "integer",
          "description": "Specifies the maximum interval between requests sent to the server as a power of 2 in seconds.",
          "minimum": -6,
          "maximum": 24
        },
        "iburst": {
          "type": "boolean",
          "default": true,
          "description": "Configures chronyd behavior related to burst requests on startup."
        },
        "prefer": {
          "type": "boolean",
          "default": false,
          "description": "Prefer this source over sources without the prefer option."
        }
      }
    }
  },
  "leapsectz": {
    "type": "string",
    "description": "Timezone used by chronyd to determine when will the next leap second occur. Empty value will remove the option."
  }
}
"""


DELETE_TIME_SOURCE_LINE_REGEX = re.compile(r"(server|pool|peer) ")
DELETE_LEAPSECTZ_LINE_REGEX = re.compile(r"leapsectz ")


# In-place modify the passed 'chrony_conf_lines' by removing lines which
# match the provided regular expression.
def delete_config_lines(chrony_conf_lines, compiled_re):
    chrony_conf_lines[:] = [line for line in chrony_conf_lines if not compiled_re.match(line)]


# Modifies the passed 'chrony_conf_lines' in-place.
def handle_timeservers(chrony_conf_lines, timeservers):
    # prepend new server lines
    new_lines = [f"server {server} iburst" for server in timeservers]
    chrony_conf_lines[:] = new_lines + chrony_conf_lines


# Modifies the passed 'chrony_conf_lines' in-place.
def handle_servers(chrony_conf_lines, servers):
    new_lines = []

    for server in servers:
        new_line = f"server {server['hostname']}"
        if server.get("prefer", False):
            new_line += " prefer"
        if server.get("iburst", True):
            new_line += " iburst"
        # Default to 'None', because the value can be zero.
        minpoll_value = server.get("minpoll", None)
        if minpoll_value is not None:
            new_line += f" minpoll {minpoll_value}"
        # Default to 'None', because the value can be zero.
        maxpoll_value = server.get("maxpoll", None)
        if maxpoll_value is not None:
            new_line += f" maxpoll {maxpoll_value}"
        new_lines.append(new_line)

    chrony_conf_lines[:] = new_lines + chrony_conf_lines


# Modifies the passed 'chrony_conf_lines' in-place.
def handle_leapsectz(chrony_conf_lines, timezone):
    # Delete the directive as the first step, to prevent the situation of
    # having it defined multiple times in the configuration.
    delete_config_lines(chrony_conf_lines, DELETE_LEAPSECTZ_LINE_REGEX)

    if timezone:
        chrony_conf_lines[:] = [f"leapsectz {timezone}"] + chrony_conf_lines


def main(tree, options):
    timeservers = options.get("timeservers", [])
    servers = options.get("servers", [])
    # Empty string value will remove the option from the configuration,
    # therefore default to 'None' to distinguish these two cases.
    leapsectz = options.get("leapsectz", None)

    with open(f"{tree}/etc/chrony.conf") as f:
        chrony_conf = f.read()

    # Split to lines and remove ones starting with server, pool or peer.
    # At least one option configuring NTP servers is required, therefore
    # we do it before applying the configuration.
    lines = chrony_conf.split('\n')
    delete_config_lines(lines, DELETE_TIME_SOURCE_LINE_REGEX)

    if timeservers:
        handle_timeservers(lines, timeservers)
    if servers:
        handle_servers(lines, servers)
    if leapsectz is not None:
        handle_leapsectz(lines, leapsectz)

    new_chrony_conf = "\n".join(lines)

    with open(f"{tree}/etc/chrony.conf", "w") as f:
        f.write(new_chrony_conf)

    return 0


if __name__ == '__main__':
    args = osbuild.api.arguments()
    r = main(args["tree"], args["options"])
    sys.exit(r)
